package com.ams.studio.xml;

import com.ams.studio.xml.annotation.ContractNode;
import org.apache.commons.lang.StringUtils;
import org.dom4j.DocumentException;
import org.dom4j.io.OutputFormat;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

/**
 * 实现 xml -- object 之间的转换。使用 @ContractNode 作为映射关系. xmlToObject 支持循环节点到 List 类型属性的映射
 * <p>
 * <p/>
 * <p>
 * Created by zhongyl on 2015/12/3.
 */
public class XmlContractParser {

    private boolean enableNodeSorting;

    public void xmlToObject(Object instance, String xml) throws DocumentException {
        xmlToObject(instance, xml, null);
    }

    public void xmlToObject(Object instance, String xml, String encoding) throws DocumentException {
        XmlHelper helper = new XmlHelper();
        helper.load(xml, encoding);
        doXmlToObject(instance, helper);
    }

    private void doXmlToObject(Object instance, XmlHelper helper) {
        List<ObjectInfo> bizNodeFields = ObjectInfoCache.getObjectInfo(instance.getClass());
        if (bizNodeFields == null || bizNodeFields.size() == 0) {
            return;
        }
        try {
            for (ObjectInfo item : bizNodeFields) {
                setObjectValues(instance, item, helper);
            }
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(ex);
        } catch (InstantiationException ex) {
            throw new RuntimeException(ex);
        }
    }

    private void setObjectValues(Object instance, ObjectInfo item, XmlHelper helper) throws InstantiationException,
            IllegalAccessException {
        List<Field> fields = item.getContractFields();
        if (fields == null || fields.isEmpty()) {
            return;
        }
        for (Field f : fields) {
            setFieldValue(instance, f, helper, null);
        }
    }

    public void nodeContentToObject(Object instance, XmlNodeContext parent) {
        List<ObjectInfo> bizNodeFields = ObjectInfoCache.getObjectInfo(instance.getClass());
        if (bizNodeFields == null || bizNodeFields.size() == 0) {
            return;
        }
        try {
            XmlLoopContext context = parent.toLoopContext();
            context.next();
            for (ObjectInfo item : bizNodeFields) {
                setObjectValues(instance, item, context);
            }
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(ex);
        } catch (InstantiationException ex) {
            throw new RuntimeException(ex);
        }
    }

    private void setObjectValues(Object instance, ObjectInfo item, XmlLoopContext context)
            throws InstantiationException, IllegalAccessException {
        List<Field> fields = item.getContractFields();
        if (fields == null || fields.isEmpty()) {
            return;
        }
        for (Field f : fields) {
            setFieldValue(instance, f, null, context);
        }
    }

    private void setFieldValue(Object instance, Field f, XmlHelper helper, XmlLoopContext context)
            throws IllegalAccessException, InstantiationException {
        f.setAccessible(true);
        ContractNode annotation = f.getAnnotation(ContractNode.class);
        String xpath = annotation.path();
        if (StringUtils.isBlank(xpath)) {
            throw new IllegalArgumentException("xpath not set, field: " + f.getName());
        }
        if (isListField(annotation)) {
            createListField(instance, f, helper, context);
        } else {
            setSimpleFieldValue(instance, f, helper, context);
        }
    }

    private boolean isListField(ContractNode annotation) {
        return annotation.listItemType() != Object.class;
    }

    private void setSimpleFieldValue(Object instance, Field f, XmlHelper helper, XmlLoopContext context)
            throws IllegalAccessException {
        ContractNode annotation = f.getAnnotation(ContractNode.class);
        String xpath = annotation.path();
        String description = annotation.description();
        String value = getNodeValue(helper, context, annotation);
        if (!annotation.optional() && StringUtils.isBlank(value)) {

            throw new ParseException(StringUtils.isEmpty(description) ? xpath + ":值不能为空" : description +
                    "不能为空");

        }
        doSetSimpleFieldValue(instance, f, annotation, value);
    }

    private String getNodeValue(XmlHelper helper, XmlLoopContext context, ContractNode annotation) {
        String value;
        if (helper != null) {
            if (StringUtils.isBlank(annotation.attribute())) {
                value = helper.getNodeValue(annotation.path());
            } else {
                value = helper.getAttributeValue(annotation.path(), annotation.attribute());
            }
        } else {
            if (StringUtils.isBlank(annotation.attribute())) {
                value = context.getChildNodeValue(annotation.path());
            } else {
                value = context.getChildNodeAttributeValue(annotation.path(), annotation.attribute());
            }
        }
        return value;
    }

    private void doSetSimpleFieldValue(Object instance, Field f, ContractNode annotation, String value)
            throws IllegalAccessException {
        if (f.getType() == String.class) {
            f.set(instance, value);
        } else if (f.getType() == Integer.class || f.getType() == int.class) {
            f.setInt(instance, Integer.parseInt(value));
        } else if (f.getType() == Double.class || f.getType() == double.class) {
            f.setDouble(instance, Double.parseDouble(value));
        } else if (f.getType() == Float.class || f.getType() == float.class) {
            f.setFloat(instance, Float.parseFloat(value));
        } else if (f.getType() == boolean.class) {
            if (StringUtils.equals(value, "0") || StringUtils.equalsIgnoreCase(value, "N")) {
                f.setBoolean(instance, false);
            } else if (StringUtils.equals(value, "1") || StringUtils.equalsIgnoreCase(value, "Y")) {
                f.setBoolean(instance, true);
            } else if (StringUtils.isNotBlank(value)) {
                f.setBoolean(instance, Boolean.parseBoolean(value));
            }
        } else if (f.getType() == Boolean.class) {
            if (StringUtils.equals(value, "0") || StringUtils.equalsIgnoreCase(value, "N")) {
                f.set(instance, Boolean.FALSE);
            } else if (StringUtils.equals(value, "1") || StringUtils.equalsIgnoreCase(value, "Y")) {
                f.set(instance, Boolean.TRUE);
            } else if (StringUtils.isNotBlank(value)) {
                f.set(instance, Boolean.parseBoolean(value));
            }
        } else if (f.getType() == Date.class) {
            if (!StringUtils.isBlank(annotation.format())) {
                f.set(instance, Util.parseTime(value, annotation.format()));
            } else {
                throw new IllegalArgumentException("format cannot be null for date field");
            }
        } else {
            throw new RuntimeException("not supported field type" + f.getName());
        }
    }

    /**
     * helper 与 context 必有一个为 null, 另一个不为 null.
     *
     * @param instance      instance.
     * @param f             field.
     * @param helper        helper nullable.
     * @param parentContext parent context, nullable.
     * @throws InstantiationException when assign.
     * @throws IllegalAccessException when assign.
     */
    private void createListField(Object instance, Field f, XmlHelper helper, XmlLoopContext parentContext)
            throws InstantiationException, IllegalAccessException {
        ContractNode annotation = f.getAnnotation(ContractNode.class);
        List<ObjectInfo> listItemFields = ObjectInfoCache.getObjectInfo(annotation.listItemType());
        if (listItemFields == null || listItemFields.isEmpty()) {
            return;
        }
        List list = new ArrayList();
        XmlLoopContext context;
        if (helper != null) {
            context = helper.getNodes(annotation.path());
        } else {
            context = parentContext.getHelper().getNodes(parentContext, annotation.path());
        }
        while (context.next()) {
            Object item = createListItem(annotation.listItemType(), context);
            list.add(item);
        }
        f.set(instance, list);
    }

    private Object createListItem(Class listItemType, XmlLoopContext context) throws IllegalAccessException,
            InstantiationException {
        Object itemInstance = listItemType.newInstance();
        List<ObjectInfo> listItemFields = ObjectInfoCache.getObjectInfo(listItemType);
        for (ObjectInfo item : listItemFields) {
            for (Field f : item.getContractFields()) {
                setFieldValue(itemInstance, f, null, context);
            }
        }
        return itemInstance;
    }

    public static OutputFormat buildFormatted(String encoding) {
        OutputFormat format = new OutputFormat();
        format.setEncoding(encoding);
        format.setSuppressDeclaration(false);
        format.setIndent(true);
        format.setNewlines(true);
        return format;
    }

    public String objectToXml(Object instance, String encoding) {
        OutputFormat format = new OutputFormat();
        format.setEncoding(encoding);
        format.setSuppressDeclaration(true);
        format.setIndent(false);
        return objectToXml(instance, null, format);
    }

    public String objectToXml(Object instance, XmlNodeContext parentNode, String encoding) {
        OutputFormat format = new OutputFormat();
        format.setEncoding(encoding);
        format.setSuppressDeclaration(true);
        format.setIndent(false);
        return objectToXml(instance, parentNode, format);
    }

    public String objectToXml(Object instance, OutputFormat format) {
        return objectToXml(instance, null, format);
    }

    public String objectToXml(Object instance, XmlNodeContext parentNode, OutputFormat format) {
        XmlHelper helper = parentNode == null ? new XmlHelper() : parentNode.getHelper();
        doObjectToXml(instance, null, helper, parentNode);
        return helper.export(format);
    }

    private void doObjectToXml(Object instance, Class instanceType, XmlHelper helper, XmlNodeContext parentNode) {
        Class<?> clazz = instanceType == null ? instance.getClass() : instanceType;
        List<ObjectInfo> bizNodeFields = ObjectInfoCache.getObjectInfo(clazz);
        if (bizNodeFields == null || bizNodeFields.size() == 0) {
            return;
        }
        try {
            for (ObjectInfo item : bizNodeFields) {
                partialFieldsToXml(instance, item, helper, parentNode);
            }
        } catch (IllegalAccessException ex) {
        }
    }

    private void partialFieldsToXml(Object instance, ObjectInfo objectInfo, XmlHelper helper, XmlNodeContext parentNode)
            throws IllegalAccessException {
        List<Field> fields = objectInfo.getContractFields();
        if (fields == null || fields.size() == 0) {
            return;
        }

        if (enableNodeSorting) {
            Collections.sort(fields, new Comparator<Field>() {
                public int compare(Field o1, Field o2) {
                    ContractNode a1 = o1.getAnnotation(ContractNode.class);
                    ContractNode a2 = o2.getAnnotation(ContractNode.class);
                    return a1.order() - a2.order();
                }
            });
        }

        for (Field f : fields) {
            ContractNode annotation = f.getAnnotation(ContractNode.class);
            validateFieldAnnotation(annotation, f.getName());
            if (isListField(annotation)) {
                listFieldToNodes(instance, f, helper, parentNode);
            } else {
                simpleFieldToNode(instance, f, helper, parentNode);
            }
        }
    }

    private void listFieldToNodes(Object instance, Field field, XmlHelper helper, XmlNodeContext parentNode)
            throws IllegalAccessException {
        field.setAccessible(true);
        Object fieldValue = field.get(instance);
        if (fieldValue == null) {
            return;
        }
        List<?> list = (List<?>) fieldValue;
        if (list == null || list.isEmpty()) {
            return;
        }
        ContractNode annotation = field.getAnnotation(ContractNode.class);
        if (annotation.listItemType().isPrimitive() || annotation.listItemType() == String.class) {
            // 如果是 List<int> 或 List<Integer> 或 List<String> 之类的，则单独处理

            simpleListToXml(list, annotation, helper, parentNode);
        } else {
            for (Object item : list) {
                XmlNodeContext subParentNode = helper.addNode(parentNode, annotation.path(), null);
                doObjectToXml(item, annotation.listItemType(), helper, subParentNode);
            }
        }
    }

    private void simpleListToXml(List<?> list, ContractNode annotation, XmlHelper helper, XmlNodeContext parentNode) {
        for (Object item : list) {
            helper.addNode(parentNode, annotation.path(), item.toString());
        }
    }

    private void validateFieldAnnotation(ContractNode annotation, String fieldName) {
        String xpath = annotation.path();
        if (StringUtils.isBlank(xpath)) {
            throw new IllegalArgumentException("xpath not set, field: " + fieldName);
        }
    }

    private void simpleFieldToNode(Object instance, Field f, XmlHelper helper, XmlNodeContext parentNode)
            throws IllegalAccessException {
        ContractNode annotation = f.getAnnotation(ContractNode.class);
        String xpath = annotation.path();
        String value = getFieldStringValue(instance, f, annotation);
        if (StringUtils.isBlank(annotation.attribute())) {
            helper.insertOrUpdateNode(parentNode, xpath, value);
        } else {
            helper.insertOrUpdateNodeAttribute(parentNode, xpath, annotation.attribute(), value);
        }
    }

    private String getFieldStringValue(Object instance, Field field, ContractNode annotation)
            throws IllegalAccessException {
        field.setAccessible(true);
        Object fieldValue = field.get(instance);
        if (fieldValue == null) {
            return null;
        }

        if (field.getType() == String.class) {
            return (String) fieldValue;
        }

        if (field.getType() == Date.class) {
            if (!StringUtils.isBlank(annotation.format())) {
                return Util.parseDate((Date) fieldValue, annotation.format());
            } else {
                return fieldValue.toString();
            }
        }
        if (field.getType() == Double.class || field.getType() == double.class) {
            return Util.decimalToString(new BigDecimal((Double) fieldValue), 2);
        }
        return fieldValue.toString();
    }

    public boolean isEnableNodeSorting() {
        return enableNodeSorting;
    }

    public void setEnableNodeSorting(boolean enableNodeSorting) {
        this.enableNodeSorting = enableNodeSorting;
    }

}
